# Mobile login verification with deep links

> Supacharger supports verified HTTPS links for mobile authentication. iOS calls these Universal Links; Android calls them App Links. The same email URL opens the installed app when the platform association succeeds and falls back to the web route when the app is unavailable.

# Mobile login verification with deep links

Supacharger supports verified HTTPS links for mobile authentication. iOS calls these Universal Links; Android calls them App Links. The same email URL opens the installed app when the platform association succeeds and falls back to the web route when the app is unavailable.

This guide covers magic-link sign-in, link-based email confirmation, and password-reset callbacks. An email OTP is different: the user copies or autofills the configured-length code into the Supacharger form, so that flow does not need to leave and reopen the app.

## How the callback works

```text
app → Supabase Auth → email client
                         ↓
installed app ← verified HTTPS callback → browser fallback
       ↓
same initiating client exchanges the one-use PKCE code
```

Supacharger's `/auth/callback` Route Handler exchanges the `code` for a session. Supabase PKCE codes expire quickly, can be exchanged only once, and require the verifier stored by the client that started the flow. Therefore:

- a wrapped Supacharger web app must load the incoming URL in the same persistent web view and cookie store that requested the email;
- a fully native app must start and complete the flow with the same native Supabase client and secure storage adapter; and
- neither implementation should copy access tokens, refresh tokens, or PKCE verifiers into logs, analytics, application metadata, or another URL.

See Supabase's [PKCE flow](https://supabase.com/docs/guides/auth/sessions/pkce-flow), [native mobile deep linking](https://supabase.com/docs/guides/auth/native-mobile-deep-linking), and [redirect URL](https://supabase.com/docs/guides/auth/redirect-urls) guidance.

## Configure Supacharger

Edit the developer-owned `src/supacharger.config.ts` (`supacharger.config.ts` in Specdrive). Replace every example value with identifiers belonging to the application being signed:

```ts
MOBILE_DEEP_LINKING: {
  ENABLED: true,
  ASSOCIATED_PATHS: [
    '/auth/callback',
    '/auth/confirm',
  ],
  IOS: {
    APP_IDS: ['ABCDE12345.com.example.myapp'],
  },
  ANDROID: {
    APPS: [
      {
        PACKAGE_NAME: 'com.example.myapp',
        SHA256_CERT_FINGERPRINTS: [
          '<PLAY_APP_SIGNING_SHA256_FINGERPRINT>',
        ],
      },
    ],
  },
},
```

`IOS.APP_IDS` uses `<Apple App ID prefix>.<bundle identifier>`, not the numeric App Store ID. `ANDROID.APPS` can contain separate debug, staging, or production packages. A package can contain multiple fingerprints during an intentional signing-key transition.

The example Apple prefix, bundle identifier, Android package, domain, and fingerprint are placeholders. Public documentation must never reproduce an application's real values merely to provide an example.

When a platform has no configured identifiers, its well-known endpoint returns `404`. Once enabled and configured, Supacharger generates current platform documents at:

```text
https://app.example.com/.well-known/apple-app-site-association
https://app.example.com/.well-known/assetlinks.json
```

Each URL must respond directly over HTTPS with status `200`, `Content-Type: application/json`, and no redirect. Configure every production subdomain independently. The generated iOS document uses Apple's current `appIDs` and `components` format; the Android document uses `delegate_permission/common.handle_all_urls`.

## Configure Supabase Auth

In each hosted Supabase environment:

1. Set **Authentication → URL Configuration → Site URL** to the canonical production web origin, such as `https://app.example.com`.
2. Add exact production redirect URLs for every enabled flow:

   ```text
   https://app.example.com/auth/callback
   https://app.example.com/auth/confirm
   ```

3. Use broad `/**` patterns only for local development or deployment previews. Prefer exact production paths.
4. If an email template constructs its own confirmation URL while the application supplies `emailRedirectTo`, use Supabase's `{{ .RedirectTo }}` variable as documented. Keep `{{ .Token }}` for the OTP template.
5. Configure hosted settings separately from `supabase/config.toml`; the repository file affects only the local stack.

Supacharger's magic-link operation sends users to `/auth/callback`. Password recovery uses `/auth/callback?flow=recovery`, so it is covered by the same callback path and does not associate the internal `/account/reset-password/new` page. Link-based sign-up confirmation uses `/auth/confirm` with `token_hash` and `type`. OTP sign-up calls `verifyOtp` with the entered email and token and does not use the well-known endpoints.

## iOS Universal Links

Follow Apple's [associated domains](https://developer.apple.com/documentation/xcode/supporting-associated-domains) and [Universal Link](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) guidance:

1. In Xcode, select the native target and add **Signing & Capabilities → Associated Domains**.
2. Add the exact host without a scheme, path, query, or trailing slash:

   ```text
   applinks:app.example.com
   ```

3. Confirm that the signed target's application identifier exactly matches an `IOS.APP_IDS` entry.
4. Accept only the expected HTTPS host and paths when continuing the user activity. A web-wrapper bridge can use this shape:

   ```swift
   .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
       guard let url = activity.webpageURL,
             url.scheme == "https",
             url.host == "app.example.com",
             ["/auth/callback", "/auth/confirm"]
                 .contains(url.path)
       else { return }

       authWebView.load(URLRequest(url: url))
   }
   ```

Use the web view and data store that initiated authentication. A native Swift client should instead give the verified URL to its native auth coordinator and complete the code exchange there.

Apple fetches the association through its CDN and may cache it. The file must be named `apple-app-site-association` without a `.json` extension. Apple's [Universal Link diagnostics](https://developer.apple.com/documentation/technotes/tn3155-debugging-universal-links) recommend:

```bash
sudo swcutil dl -d app.example.com
sudo swcutil verify -d app.example.com -j ./apple-app-site-association \
  -u 'https://app.example.com/auth/callback'
```

On a device, paste the link into Notes and long-press it. Typing the URL directly into Safari's address bar intentionally remains browser navigation and is not a valid Universal Link test.

## Android App Links

Follow Android's [App Link intent-filter](https://developer.android.com/training/app-links/add-applinks), [website association](https://developer.android.com/training/app-links/configure-assetlinks), and [verification](https://developer.android.com/training/app-links/verify-applinks) guidance.

Use the application ID from the native module's Gradle configuration. When Google Play App Signing is enabled, use the app-signing certificate fingerprint shown by Play Console—not the local upload-key fingerprint. Fingerprints are uppercase, colon-separated SHA-256 values.

Declare verified HTTPS paths in `AndroidManifest.xml`. Separate filters avoid accidental combinations when hosts or path rules later diverge:

```xml
<activity
    android:name=".MainActivity"
    android:exported="true">

    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="https"
            android:host="app.example.com"
            android:pathPrefix="/auth/" />
    </intent-filter>
</activity>
```

The generated `assetlinks.json` proves the package/domain relationship. On Android versions before dynamic App Links, the native manifest remains responsible for path restrictions, so keep it aligned with `ASSOCIATED_PATHS`.

Handle both a cold start and a new intent, then validate the URL again before loading or exchanging anything:

```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    handleAuthLink(intent)
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    setIntent(intent)
    handleAuthLink(intent)
}

private fun handleAuthLink(intent: Intent) {
    val uri = intent.data ?: return
    val allowedPaths = setOf(
        "/auth/callback",
        "/auth/confirm",
    )

    if (uri.scheme != "https" ||
        uri.host != "app.example.com" ||
        uri.path !in allowedPaths
    ) return

    authWebView.loadUrl(uri.toString())
}
```

A fully native Kotlin client should pass the validated URI to its native auth coordinator instead of a web view.

After installing the signed build, wait for verification and run:

```bash
adb shell pm set-app-links --package com.example.myapp 0 all
adb shell pm verify-app-links --re-verify com.example.myapp
adb shell pm get-app-links com.example.myapp
adb shell am start -W -a android.intent.action.VIEW \
  -c android.intent.category.BROWSABLE \
  -d 'https://app.example.com/auth/callback?code=test'
```

The host should report `verified`. A `legacy_failure`, browser chooser, or browser-only result usually means the deployed file redirected, the package did not match, the wrong signing certificate was used, or the manifest host/path differed.

## Deployment and security checklist

- Deploy the well-known routes before shipping a native build that declares the domain.
- Use exact production hosts and callback paths; do not accept arbitrary `next`, host, scheme, or path values.
- Preserve the full callback query string, but never log it. Auth codes and token hashes are short-lived credentials.
- Keep the callback in the client that initiated PKCE. Do not attempt a second exchange after the code has been consumed.
- Test installed and uninstalled behaviour. Without the app, the same HTTPS URL must complete safely in the browser.
- Test cold start, warm start, expired links, cancelled sign-in, staging and production signing, and password reset separately.
- Do not enable Android with a guessed package or fingerprint. Do not enable iOS with another application's App ID.
- Re-test after changing domains, native application identifiers, signing certificates, callback paths, email templates, or Supabase redirect settings.
